/*******************************************************************************
 *  Copyright (c) 2010 Chase Technology Ltd.
 *  
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the GNU General Public License v3.0
 *  which is available at http://www.gnu.org/licenses/
 * 
 * Contributors:
 *     Doug Satchwell - initial API and implementation
 *******************************************************************************/
package org.jfunction;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.jfunction.annotations.Step;
import org.jfunction.annotations.StepContext;

public class Scenario {
	private final List<ExecutableStep> steps = new ArrayList<ExecutableStep>();
	private final Class<?>[] classes;
	private String description;
	
	public Scenario(Class<?>... classes) {
		this.classes = classes;
	}

	public void setDescription(String text) {
		this.description = text;
	}
	
	public String getDescription() {
		return description;
	}
	
	public List<ExecutableStep> getSteps() {
		return steps;
	}

	public void addStep(String text) throws JFunctionException {
		for (Class<?> clazz : classes) {
			for (Method method : clazz.getMethods()) {
				Step step = method.getAnnotation(Step.class);
				Map<String,String> varNames = new LinkedHashMap<String,String>();
				if (step != null && matches(step.value(),text,varNames)) {
					if (method.getParameterTypes().length > varNames.size()) {
						throw new JFunctionException("Method '"+method.getName()+"' of class '"+clazz.getName()+"' has more parameters than specified in the step");
					} else if (method.getParameterTypes().length < varNames.size()) {
						throw new JFunctionException("Method '"+method.getName()+"' of class '"+clazz.getName()+"' has too few parameters for the step");
					}
					steps.add(new ExecutableStep(method, varNames));
					return;
				}
			}
		}
		throw new JFunctionException("Could not find a step that matches text: '"+text+"'");
	}

	public static boolean matches(String value, String text, Map<String,String> varNames) {
		value = value.trim();
		text = text.trim();
		int valueIndex = 0;
		int textIndex = 0;
		while (valueIndex < value.length()) {
			char valueChar = value.charAt(valueIndex++);
			char textChar = text.charAt(textIndex++);
			if (valueChar != textChar) {
				return false;
			}
			if (valueChar == '<') {
				int newValueIndex = readAhead(value,valueIndex);
				int newTextIndex = readAhead(text,textIndex);
				String valueVar = value.substring(valueIndex,newValueIndex);
				String textVar = text.substring(textIndex,newTextIndex);
				valueIndex = newValueIndex + 1;
				textIndex = newTextIndex + 1;
				varNames.put(valueVar, textVar);
			}
		}
		return (valueIndex == value.length()) && (textIndex == text.length());
	}

	private static int readAhead(String value, int index) {
		while(index < value.length()) {
			char c = value.charAt(++index);
			if (c == '>') {
				break;
			}
		}
		return index;
	}

	public void run(Example example) {
		Example.setCurrent(example);
		// create an instance of each class that will persist during this example only
		Map<Class<?>,Object> instances = new HashMap<Class<?>,Object>();
		for (ExecutableStep step : steps) {
			Class<?> declaringClass = step.getMethod().getDeclaringClass();
			if (!instances.containsKey(declaringClass)) {
				try {
					Object instance = declaringClass.newInstance();
					instances.put(declaringClass, instance);
				} catch (InstantiationException e) {
					throw new JFunctionException(e);
				} catch (IllegalAccessException e) {
					throw new JFunctionException(e);
				}
			}
		}
		for (int i=0;i<steps.size();i++) {
			ExecutableStep step = steps.get(i);
			Class<?> declaringClass = step.getMethod().getDeclaringClass();
			Object obj = instances.get(declaringClass);
			Object[] args = step.getArgs(example);
			try {
				// read the context
				for (Field field : declaringClass.getDeclaredFields()) {
					StepContext sc = field.getAnnotation(StepContext.class);
					if (sc != null) {
						String key = sc.value();
						if ("".equals(key)) {
							key = field.getName();
						}
						if (!sc.optional() && !step.hasValue(example,key)) {
							throw new JFunctionException("A context value with key '"+key+"' could not be found for field '"+field.getName()+"' of class '"+declaringClass+"'\nEither set a value in the context or mark the StepContext annotation with a type of OPTIONAL.");
						}
						Object value = step.getValue(example,key);
						field.setAccessible(true);
						field.set(obj, value);
					}
				}
				step.getMethod().invoke(obj, args);
				// write the context
				for (Field field : declaringClass.getDeclaredFields()) {
					StepContext sc = field.getAnnotation(StepContext.class);
					if (sc != null) {
						String key = sc.value();
						if ("".equals(key)) {
							key = field.getName();
						}
						field.setAccessible(true);
						Object value = field.get(obj);
						step.setValue(example, key, value);
					}
				}
			} catch (IllegalArgumentException e) {
				throw new JFunctionException(e);
			} catch (IllegalAccessException e) {
				throw new JFunctionException(e);
			} catch (InvocationTargetException e) {
				throw new JFunctionException(e.getCause());
			}
		}
	}
}
